/*
 * Copyright (c) 2009, Hamish Morgan. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the University of Sussex nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package locusts.client;

import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import javax.swing.KeyStroke;
import locusts.lib.MathUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class that handles key controls for changing the clients view. It supports
 * all standard <tt>AffineTransform</tt> operations: translate (scroll),
 * rotate, scale (zoom), and shearing. It also proved for a reset operation.
 *
 * The resultant transform is smoothed for the current transform, to the
 * target (as indicated by user key presses.)
 *
 * Objects of type <tt>ViewTransformKeyListener</tt> are synchronized
 * (thread-safe), by employing synchronisation. This is imperitive since it
 * is likely that the it will be accessed concurrently both by key events
 * and by the update cycle.
 *
 * The keyboard map is laoded from the properties file keys.properties.
 *
 * @author Hamish Morgan
 */
public class ViewTransformKeyListener extends KeyAdapter implements Cloneable {

    private static final Logger LOG = LoggerFactory.getLogger(
            ViewTransformKeyListener.class);
    private static final String KEYS_FILE = "res/keys.properties";
    private static final String KEY_SCROLL_LEFT_NAME = "scrollLeft";
    private static final String KEY_SCROLL_RIGHT_NAME = "scrollRight";
    private static final String KEY_SCROLL_UP_NAME = "scrollUp";
    private static final String KEY_SCROLL_DOWN_NAME = "scrollDown";
    private static final String KEY_ZOOM_IN_NAME = "zoomIn";
    private static final String KEY_ZOOM_OUT_NAME = "zoomOut";
    private static final String KEY_ROTATE_ANTICLOCKWISE_NAME =
            "rotateAnticlockwise";
    private static final String KEY_ROTATE_CLOCKWISE_NAME = "rotateClockwise";
    private static final String KEY_SHEAR_UP_NAME = "shearUp";
    private static final String KEY_SHEAR_DOWN_NAME = "shearDown";
    private static final String KEY_SHEAR_LEFT_NAME = "shearLeft";
    private static final String KEY_SHEAR_RIGHT_NAME = "shearRight";
    private static final String KEY_RESET_NAME = "reset";
    private final Properties keys = new Properties();
    //
    private static final double DEFAULT_INERTIA = 0.01;
    private static final double DEFAULT_TRANSLATE_AMOUNT = 1000;
    private static final double DEFAULT_SHEAR_AMOUNT = 1;
    private static final double DEFAULT_SCALE_AMOUNT = 2;
    private static final double DEFAULT_ROTATE_AMOUNT = Math.PI;
    private static final double ROUNDING_EPSILON = 0.0001d;
    //
    private int scrollLeftKey = KeyEvent.VK_LEFT;
    private int scrollRightKey = KeyEvent.VK_RIGHT;
    private int scrollUpKey = KeyEvent.VK_UP;
    private int scrollDownKey = KeyEvent.VK_DOWN;
    private int zoomInKey = KeyEvent.VK_EQUALS;
    private int zoomOutKey = KeyEvent.VK_MINUS;
    private int rotateAnticlockwiseKey = KeyEvent.VK_OPEN_BRACKET;
    private int rotateClockwiseKey = KeyEvent.VK_CLOSE_BRACKET;
    private int shearUpKey = KeyEvent.VK_W;
    private int shearDownKey = KeyEvent.VK_S;
    private int shearLeftKey = KeyEvent.VK_A;
    private int shearRightKey = KeyEvent.VK_D;
    private int resetKey = KeyEvent.VK_0;
    //
    private double inertia = DEFAULT_INERTIA;
    private double translateSpeed = DEFAULT_TRANSLATE_AMOUNT;
    private double shearSpeed = DEFAULT_SHEAR_AMOUNT;
    private double scaleSpeed = DEFAULT_SCALE_AMOUNT;
    private double rotateSpeed = DEFAULT_ROTATE_AMOUNT;
    //
    private final Set<KeyStroke> heldKeys;
    private final AffineTransform vTranslate;
    private final AffineTransform vRotation;
    private final AffineTransform vScale;
    private final AffineTransform vShear;
    private AffineTransform transform;
    private double viewCenterX;
    private double viewCenterY;
    private boolean updateRequired;
    //

    /**
     * Constructor that creates a new instance of
     * <tt>ViewTransformKeyListener</tt> and populates it with default values.
     * These can be changed later by called mutator methods.
     */
    public ViewTransformKeyListener() {
        inertia = DEFAULT_INERTIA;
        translateSpeed = DEFAULT_TRANSLATE_AMOUNT;
        shearSpeed = DEFAULT_SHEAR_AMOUNT;
        scaleSpeed = DEFAULT_SCALE_AMOUNT;
        rotateSpeed = DEFAULT_ROTATE_AMOUNT;
        heldKeys = Collections.synchronizedSet(new HashSet<KeyStroke>());
        transform = new AffineTransform();
        vTranslate = new AffineTransform();
        vRotation = new AffineTransform();
        vScale = new AffineTransform();
        vShear = new AffineTransform();
        viewCenterX = 0;
        viewCenterY = 0;
        updateRequired = false;
        loadKeys();
    }

    /**
     * Constructor that should only be used internally and by subclasses
     * for cloning.
     *
     * @param other the <tt>ViewTransformKeyListener</tt> to clone.
     */
    protected ViewTransformKeyListener(ViewTransformKeyListener other) {
        this.inertia = other.inertia;
        this.translateSpeed = other.translateSpeed;
        this.shearSpeed = other.shearSpeed;
        this.scaleSpeed = other.scaleSpeed;
        this.rotateSpeed = other.rotateSpeed;
        this.heldKeys = Collections.synchronizedSet(new HashSet<KeyStroke>());
        this.vTranslate = other.vTranslate;
        this.vRotation = other.vRotation;
        this.vScale = other.vScale;
        this.vShear = other.vShear;
        this.viewCenterX = other.viewCenterX;
        this.viewCenterY = other.viewCenterY;
        this.updateRequired = other.updateRequired;
    }

    private void loadKeys() {
        LOG.debug("Loading keyboard layout file: {}", KEYS_FILE);
        try {
            keys.load(new FileInputStream(KEYS_FILE));
        } catch (IOException e) {
            LOG.error("Failed to load keyboard properties file.", e);
        }
        try {
            if (keys.containsKey(KEY_SCROLL_LEFT_NAME))
                scrollLeftKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_SCROLL_LEFT_NAME)).getKeyCode();
            if (keys.containsKey(KEY_SCROLL_RIGHT_NAME))
                scrollRightKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_SCROLL_RIGHT_NAME)).getKeyCode();
            if (keys.containsKey(KEY_SCROLL_UP_NAME))
                scrollUpKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_SCROLL_UP_NAME)).getKeyCode();
            if (keys.containsKey(KEY_SCROLL_DOWN_NAME))
                scrollDownKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_SCROLL_DOWN_NAME)).getKeyCode();
            if (keys.containsKey(KEY_ZOOM_IN_NAME))
                zoomInKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_ZOOM_IN_NAME)).getKeyCode();
            if (keys.containsKey(KEY_ZOOM_OUT_NAME))
                zoomOutKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_ZOOM_OUT_NAME)).getKeyCode();
            if (keys.containsKey(KEY_ROTATE_ANTICLOCKWISE_NAME))
                rotateAnticlockwiseKey = KeyStroke.getKeyStroke(keys.
                        getProperty(
                        KEY_ROTATE_ANTICLOCKWISE_NAME)).getKeyCode();
            if (keys.containsKey(KEY_ROTATE_CLOCKWISE_NAME))
                rotateClockwiseKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_ROTATE_CLOCKWISE_NAME)).getKeyCode();
            if (keys.containsKey(KEY_SHEAR_UP_NAME))
                shearUpKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_SHEAR_UP_NAME)).getKeyCode();
            if (keys.containsKey(KEY_SHEAR_DOWN_NAME))
                shearDownKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_SHEAR_DOWN_NAME)).getKeyCode();
            if (keys.containsKey(KEY_SHEAR_LEFT_NAME))
                shearLeftKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_SHEAR_LEFT_NAME)).getKeyCode();
            if (keys.containsKey(KEY_SHEAR_RIGHT_NAME))
                shearRightKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_SHEAR_RIGHT_NAME)).getKeyCode();
            if (keys.containsKey(KEY_RESET_NAME))
                resetKey = KeyStroke.getKeyStroke(keys.getProperty(
                        KEY_RESET_NAME)).getKeyCode();
        } catch (NullPointerException e) {
            LOG.error("Failed to load the keyboard properties file because " +
                    "it contains invalid key codes.");
        }
    }

    @Override
    public void keyPressed(final KeyEvent event) {
        heldKeys.add(KeyStroke.getKeyStroke(event.getKeyCode(), event.
                getModifiers()));
    }

    @Override
    public void keyReleased(final KeyEvent event) {
        heldKeys.remove(KeyStroke.getKeyStroke(event.getKeyCode(), event.
                getModifiers()));
    }

    /**
     * Update the viewable area that this object is responsible for
     * transforming. This is nessesary since rotation and scale should be
     * done relative to the center of the view.
     *
     * The <tt>x</tt> and <tt>y</tt> arguments are not used, however they are
     * included for simplicity since this method would normally called by
     * and AWT <tt>Component.setBounds()</tt> method.
     *
     * @param x         the new x-coordinate of the attached component
     * @param y         the new y-coordinate of the attached component
     * @param width     the new width of the attached component
     * @param height    the new height of the attached component
     */
    public void setBounds(final int x, final int y, final int width,
            final int height) {
        viewCenterX = width / 2d;
        viewCenterY = height / 2d;
        translateSpeed = Math.max(width, height);
    }

    /**
     * Return the transform that should be applied the application view.
     *
     * @return the transform.
     */
    public AffineTransform getTransform() {
        return (AffineTransform)transform.clone();
    }

    /**
     * Ckeck the keys which are currently held down, and update the target
     * transform respectively. Then update the current transform by performing
     * a weighted average of it and the target transform.
     * 
     * This method should be run in main loop of the application, once per
     * normal update cycle.
     *
     * @param delta     The time in seconds that has ellapsed since the last
     *                  call to <tt>update</tt>.
     * @throws IllegalArgumentException when the argument is infinite or NaN.
     */
    public void update(double delta) {
        if (Double.isNaN(delta) || Double.isInfinite(delta))
            throw new IllegalArgumentException(
                    "Argument delta must be a finite number.");

        if (delta == 0)
            return;

        updateKeyValues(delta);

        if (!updateRequired)
            return;

        AffineTransform viewTransformTarget = new AffineTransform();
        viewTransformTarget.concatenate(vTranslate);
        viewTransformTarget.concatenate(vRotation);
        viewTransformTarget.concatenate(vScale);
        viewTransformTarget.concatenate(vShear);

        final double[] current = new double[6];
        transform.getMatrix(current);
        final double[] target = new double[6];
        viewTransformTarget.getMatrix(target);
        final double[] result = new double[6];

        updateRequired = false;
        final double cWeight = Math.pow(inertia, delta);
        final double tWeight = 1d - cWeight;
        for (int i = 0; i < current.length; i++) {
            result[i] = target[i] * tWeight + current[i] * cWeight;
            // if the result is very near the target, then just set it to
            // that and don't both with any more pressision
            if (MathUtil.epsilonEqual(result[i], target[i], ROUNDING_EPSILON))
                result[i] = target[i];
            else updateRequired = true;
        }
        transform = new AffineTransform(result);
    }
    
    private void updateKeyValues(double delta) {
        if (heldKeys.size() == 0)
            return;
        synchronized (heldKeys) {
            for (KeyStroke k : heldKeys) {
                updateKeyValue(k, delta);
            }
        }
    }

    private void updateKeyValue(KeyStroke key, double delta) {
        final int code = key.getKeyCode();
        if (code == scrollUpKey)
            scrollUp(delta);
        else if (code == scrollDownKey)
            scrollDown(delta);
        else if (code == scrollLeftKey)
            scrollLeft(delta);
        else if (code == scrollRightKey)
            scrollRight(delta);
        else if (code == zoomInKey)
            zoomIn(delta);
        else if (code == zoomOutKey)
            zoomOut(delta);
        else if (code == rotateClockwiseKey)
            roateClockwise(delta);
        else if (code == rotateAnticlockwiseKey)
            rotateAnticlockwise(delta);
        else if (code == shearDownKey)
            shearDown(delta);
        else if (code == shearUpKey)
            shearUp(delta);
        else if (code == shearRightKey)
            shearRight(delta);
        else if (code == shearLeftKey)
            shearLeft(delta);
        else if (code == resetKey)
            reset();
        else
            LOG.debug("Unhandled key stroke: {}", key);

    }
    //
    // =================================================================
    //
    //

    private void reset() {
        vTranslate.setToIdentity();
        vRotation.setToIdentity();
        vScale.setToIdentity();
        vShear.setToIdentity();
        updateRequired =
                true;
    }

    private void scrollUp(double delta) {
        scroll(0, translateSpeed * delta);
    }

    private void scrollDown(double delta) {
        scroll(0, -translateSpeed * delta);
    }

    private void scrollLeft(double delta) {
        scroll(translateSpeed * delta, 0);
    }

    private void scrollRight(double delta) {
        scroll(-translateSpeed * delta, 0);
    }

    private void zoomIn(double delta) {
        scale(1 + scaleSpeed * delta);
    }

    private void zoomOut(double delta) {
        scale(1d / (1 + scaleSpeed * delta));
    }

    private void shearUp(double delta) {
        shear(0, -shearSpeed * delta);
    }

    private void shearDown(double delta) {
        shear(0, shearSpeed * delta);
    }

    private void shearLeft(double delta) {
        shear(-shearSpeed * delta, 0);
    }

    private void shearRight(double delta) {
        shear(shearSpeed * delta, 0);
    }

    private void rotateAnticlockwise(double delta) {
        rotate(rotateSpeed * delta);
    }

    private void roateClockwise(double delta) {
        rotate(-rotateSpeed * delta);
    }
//
// =================================================================
//
//

    private void shear(double shx, double shy) {
        vShear.shear(shx, shy);
        updateRequired =
                true;
    }

    private void scroll(double x, double y) {
        vTranslate.translate(x, y);
        updateRequired =
                true;
    }

    private void scale(double s) {
        try {
            vScale.concatenate(vTranslate.createInverse());
            vScale.translate(viewCenterX, viewCenterY);
            vScale.scale(s, s);
            vScale.translate(-viewCenterX, -viewCenterY);
            vScale.concatenate(vTranslate);
            updateRequired =
                    true;
        } catch (NoninvertibleTransformException ex) {
            LOG.error(null, ex);
        }

    }

    private void rotate(double theta) {
        try {
            vRotation.concatenate(vTranslate.createInverse());
            vRotation.translate(viewCenterX, viewCenterY);
            vRotation.rotate(theta);
            vRotation.translate(-viewCenterX, -viewCenterY);
            vRotation.concatenate(vTranslate);
            updateRequired =
                    true;
        } catch (NoninvertibleTransformException ex) {
            LOG.error(null, ex);
        }

    }

    //
    // =================================================================
    //
    //
    /**
     * Accessor to the inertia; representing the amount that the target
     * transform is smoothed into from the current transform. This will be a
     * value between 0.0 and 1.0 inclusive; where 0.0 represents no smoothing,
     * and 1.0 represents NEVER changing to the target.
     *
     * @return the smoothing inertial
     */
    public double getInertia() {
        return inertia;
    }

    /**
     * Mutator to the inertia; representing the amount that the target
     * transform is smoothed into from the current transform. This will be a
     * value between 0.0 and 1.0 inclusive; where 0.0 represents no smoothing,
     * and 1.0 represents NEVER changing to the target.
     *
     * @param inertia the smoothing inertial
     * @throws IllegalArgumentException when the argument is infinite, NaN, or
     *      outside the rand of 0.0 to 1.0.
     */
    public void setInertia(final double inertia) {
        if (Double.isNaN(inertia) || Double.isInfinite(inertia))
            throw new IllegalArgumentException(
                    "Argument inertia must be a finite number.");
        if (inertia < 0 || inertia > 1)
            throw new IllegalArgumentException(
                    "Argument inertia must be between 0.0 and 1.0");
        this.inertia = inertia;
    }

    /**
     * Accessor to the rotatation speed in radians per second.
     * @return the rotation speed
     */
    public double getRotateSpeed() {
        return rotateSpeed;
    }

    /**
     * Mutator to the rotatation speed in radians per second.
     * @param rotateSpeed the rotation speed
     * @throws IllegalArgumentException when the argument is infinite or NaN.
     */
    public void setRotateSpeed(final double rotateSpeed) {
        if (Double.isNaN(rotateSpeed) || Double.isInfinite(rotateSpeed))
            throw new IllegalArgumentException(
                    "Argument rotateSpeed must be a finite number.");
        this.rotateSpeed = rotateSpeed;
    }

    /**
     * Accessor to the scale speed, in scale amount per second.
     * @return the scale speed
     */
    public double getScaleSpeed() {
        return scaleSpeed;
    }

    /**
     * Mutator to the scale speed, in scale amount per second.
     * @param scaleSpeed the scale speed
     * @throws IllegalArgumentException when the argument is infinite or NaN.
     */
    public void setScaleSpeed(final double scaleSpeed) {
        if (Double.isNaN(scaleSpeed) || Double.isInfinite(scaleSpeed))
            throw new IllegalArgumentException(
                    "Argument scaleSpeed must be a finite number.");
        this.scaleSpeed = scaleSpeed;
    }

    /**
     * Accessor to the shear speed, in the shear amount per second.
     * @return the shear amount.
     */
    public double getShearSpeed() {
        return shearSpeed;
    }

    /**
     * Mutator to the shear speed, in the shear amount per second.
     * @param shearSpeed the shear amount.
     * @throws IllegalArgumentException when the argument is infinite or NaN.
     */
    public void setShearSpeed(final double shearSpeed) {
        if (Double.isNaN(shearSpeed) || Double.isInfinite(shearSpeed))
            throw new IllegalArgumentException(
                    "Argument shearSpeed must be a finite number.");
        this.shearSpeed = shearSpeed;
    }

    /**
     * Accessor to the translate (scroll) speed, in pixels per second.
     * @return the translate speed
     */
    public double getTranslateSpeed() {
        return translateSpeed;
    }

    /**
     * Muratpr to the translate (scroll) speed, in pixels per second.
     * @param translateSpeed the translate speed
     * @throws IllegalArgumentException when the argument is infinite or NaN.
     */
    public void setTranslateSpeed(final double translateSpeed) {
        if (Double.isNaN(translateSpeed) || Double.isInfinite(translateSpeed))
            throw new IllegalArgumentException(
                    "Argument translateSpeed must be a finite number.");
        this.translateSpeed = translateSpeed;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        final ViewTransformKeyListener other = (ViewTransformKeyListener) obj;
        if (this.inertia != other.inertia) return false;
        if (this.translateSpeed != other.translateSpeed) return false;
        if (this.shearSpeed != other.shearSpeed) return false;
        if (this.scaleSpeed != other.scaleSpeed) return false;
        if (this.rotateSpeed != other.rotateSpeed) return false;
        if (this.heldKeys != other.heldKeys &&
                (this.heldKeys == null ||
                !this.heldKeys.equals(other.heldKeys))) return false;
        if (this.vTranslate != other.vTranslate &&
                (this.vTranslate == null ||
                !this.vTranslate.equals(other.vTranslate))) return false;
        if (this.vRotation != other.vRotation &&
                (this.vRotation == null ||
                !this.vRotation.equals(other.vRotation))) return false;
        if (this.vScale != other.vScale &&
                (this.vScale == null || !this.vScale.equals(other.vScale)))
            return false;
        if (this.vShear != other.vShear &&
                (this.vShear == null || !this.vShear.equals(other.vShear)))
            return false;
        if (this.viewCenterX != other.viewCenterX) return false;
        if (this.viewCenterY != other.viewCenterY) return false;
        if (this.updateRequired != other.updateRequired) return false;
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash =
                31 * hash + (int) (Double.doubleToLongBits(this.inertia) ^
                (Double.doubleToLongBits(this.inertia) >>> 32));
        hash =
                31 * hash +
                (int) (Double.doubleToLongBits(this.translateSpeed) ^
                (Double.doubleToLongBits(this.translateSpeed) >>> 32));
        hash =
                31 * hash +
                (int) (Double.doubleToLongBits(this.shearSpeed) ^
                (Double.doubleToLongBits(this.shearSpeed) >>> 32));
        hash =
                31 * hash +
                (int) (Double.doubleToLongBits(this.scaleSpeed) ^
                (Double.doubleToLongBits(this.scaleSpeed) >>> 32));
        hash =
                31 * hash +
                (int) (Double.doubleToLongBits(this.rotateSpeed) ^
                (Double.doubleToLongBits(this.rotateSpeed) >>> 32));
        hash =
                31 * hash +
                (this.heldKeys != null ? this.heldKeys.hashCode() : 0);
        hash =
                31 * hash +
                (this.vTranslate != null ? this.vTranslate.hashCode() : 0);
        hash =
                31 * hash +
                (this.vRotation != null ? this.vRotation.hashCode() : 0);
        hash =
                31 * hash + (this.vScale != null ? this.vScale.hashCode() : 0);
        hash =
                31 * hash + (this.vShear != null ? this.vShear.hashCode() : 0);
        hash =
                31 * hash +
                (int) (Double.doubleToLongBits(this.viewCenterX) ^
                (Double.doubleToLongBits(this.viewCenterX) >>> 32));
        hash =
                31 * hash +
                (int) (Double.doubleToLongBits(this.viewCenterY) ^
                (Double.doubleToLongBits(this.viewCenterY) >>> 32));
        hash =
                31 * hash + (this.updateRequired ? 1 : 0);
        return hash;
    }

    @Override
    public String toString() {
        final StringBuffer buf = new StringBuffer();
        buf.append(this.getClass().getSimpleName());
        buf.append('[').append(transform).append(' ');
        buf.append(", Keys[");
        synchronized (heldKeys) {
            final Iterator<KeyStroke> keyIt = heldKeys.iterator();
            if (keyIt.hasNext())
                buf.append(keyIt.next());
            while (keyIt.hasNext()) {
                buf.append(", ");
                buf.append(keyIt.next());
            }

        }
        buf.append("]]");
        return buf.toString();
    }

    @Override
    public ViewTransformKeyListener clone() {
        return new ViewTransformKeyListener(this);
    }
}
